Java 多线程导入excel数据
项目源码:
- 前端
springboo-excel - 后端
springboot-excel-process
本质上是如何高效地处理大量的数据集合:
- 从一个数据库抓取另一个数据库的大量数据
- 从大文件Excel中导入到数据库
目标分解
使用springboot,方便使用依赖和后续的文件上传
使用POI生成Excel文件
使用POI解析Excel文件
多线程拆分Excel文件,并导入到数据库,观察耗时
- 尝试使用多线程以降低耗时
- 使用 spring jdbc template,前端使用MultipartFile
- 生产者读取,消费者写入
Springboot通用配置
0.application.yml
1 | spring: |
1.application-oracle.yml,用于连接oracle数据库
1 | server: |
2.pom.xml 使用jdk1.8
1 |
|
3.swagger2.java接口调试配置
1 | package com.excel.config; |
生成Excel文件
生成简单的Excel表格
1 |
|
改进:从数据库导出到excel
使用spring jdbc Template导出excel文件
Repository层,ApInvoiceVO.java
1 | package com.excel.repository; |
Service层,ApInvoiceService.java,使用Jdbc Template
1 | /** |
Controller层
1 |
|
改进:freemarker导出到excel
上传sql文件,并且导出到excel
步骤分解
- 从file中获取sql语句
- 读取select后,from前的所有字段,生成字节码(动态生成实体类)
- 连接数据库,通过freemarker导出到excel
从Excel导入
解析简单的Excel表格
excel包含少量数据,使用Spring Jdbc Template处理,并在数据库创建记录
【注意】xls格式最大行数为65535,以二进制形式存储文件;xlsx格式以xml形式存储文件,可以最多存储100w+行数据
前端:excel.html,使用Vue
1 |
|
后端,连接的是mysql数据库
ExcelService.java
1 | package com.excel.service; |
ExcelController.java
1 | /** |
改进:添加AOP并观察单线程运行速度
尝试导入10w条数据,并观察导入的速度,添加AOP拦截
AOP配置
0.修改application.yml配置文件,修改允许上传文件的尺寸
1 | spring: |
1.pom.xml引入AOP依赖
1 | <dependency> |
2.Aspect配置类
1 | package com.excel.config; |
数据导入测试
准备了10w行的xls文档,导入到空表,观察测试结果

实际测试发现,仅能导入65535行,耗时1875ms
springboot单机导入

mysql数据总数

解析xlsx的Excel表格
pom.xml添加依赖
1 | <!-- 用于操作微软办公文档2007 --> |
添加XLSX解析的方法
1 | @PostMapping("/readExcelXLSXIntoDB") |
实际测试发现,导入10W行

mysql数据总数

多线程导入
场景:Springboot 单线程导入100w行Excel数据,报GC溢出
GC Overhead Limit Exceeded Error简介
OutOfMemoryError是java.lang.VirtualMachineError的子类,当JVM资源利用出现问题时抛出,更具体地说,这个错误是由于JVM花费太长时间执行GC且只能回收很少的堆内存时抛出的。根据Oracle官方文档,默认情况下,如果Java进程花费98%以上的时间执行GC,并且每次只有不到2%的堆被恢复,则JVM抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。
在这种情况下,用户会体验到应用程序响应非常缓慢,通常只需要几毫秒就能完成的某些操作,此时则需要更长的时间来完成,这是因为所有的CPU正在进行垃圾收集,因此无法执行其他任务。

改进:生产者消费者模型
步骤分解
获取上传的文件并保存副本到临时目录
确定长度
- 文件总长度
- 每个线程处理的任务长度
- 生产者预计线程数,根据机器性能给定,或者用 文件长度/线程任务长度
- 消费者预计线程数,根据机器性能给定,或者按比例分配(生产者:消费者 = 4:1)
数据分片处理
多线程任务一般都是需要进行数据分片
任务开始位置:
I * 线程任务长度任务结束位置:
(I+1) * 线程任务长度Excel文件处理
sheet.getRow(startPos).getRowNum()获取特定行数进行行循环Map<Integer, List<String>>定义数据结构保存Excel二维表map.get(i).add(cell.getRichStringCellValue().getString())进行列循环并保存到数据结构中- 拼接成mysql的
insert into语句,并放入生产者队列中
开启多线程
ExecutorService分别开启生产者和消费者线程CountDownLatch观察生产者任务(转换mysql语句)是否完成LinkedBlockingQueue线程安全的无界链队列,保存mysql语句供消费者调用
实例代码:
Controller
1 |
|
Service
1 |
|
进一步优化的方案
提升Excel导入速度的方法:
- 使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)
- 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间
- 使用 values(),(),() 拼接长 SQL 一次插入多行数据
- 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)
- 避免在循环中打印无用的日志
导出到Excel
从plsql中导出100w条数据,耗时745秒
多线程目标:导出100w条数据,至少优化到400秒以下

有空再做吧…
参考来源